package com.ds.filter;

import com.ds.request.ReusableBodyRequestWrapper;

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义签名过滤器
 * 签名验证过滤器，用于校验请求的合法性
 *
 * @author huahua
 * @DATE 2025/5/4
 **/
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(urlPatterns = "/**", filterName = "SignatureVerificationFilter")
@Component
@Slf4j
public class SignatureVerificationFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 签名秘钥
    @Value("${secret-key}")
    private String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // 对request进行包装，支持重复读取body
        ReusableBodyRequestWrapper requestWrapper = new ReusableBodyRequestWrapper(request);
        // 校验签名
        if (this.verifySignature(requestWrapper, response)) {
            filterChain.doFilter(requestWrapper, response);
        }
    }

    /**
     * 校验签名
     *
     * @param request HTTP请求
     * @param response HTTP响应
     * @return 签名验证结果
     * @throws IOException 如果读取请求体失败
     */
    public boolean verifySignature(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 签名
        String sign = request.getHeader("X-Sign");
        // 随机数
        String nonce = request.getHeader("X-Nonce");
        // 时间戳
        String timestampStr = request.getHeader("X-Timestamp");
        if (!StringUtils.hasText(sign) || !StringUtils.hasText(nonce) || !StringUtils.hasText(timestampStr)) {
            this.write(response, "参数错误");
            logger.error("参数错误");
            return false;
        }

        // timestamp 10分钟内有效
        long timestamp;
        try {
            timestamp = Long.parseLong(timestampStr);
        } catch (NumberFormatException e) {
            this.write(response, "时间戳格式错误");
            return false;
        }
        long currentTimestamp = System.currentTimeMillis() / 1000;
        if (Math.abs(currentTimestamp - timestamp) > 600) {
            // 将服务器当前时间和前段传递的X-Timestamp对比，校验在10分钟内有效
            this.write(response, "请求已过期");
            logger.error("请求已过期");
            return false;
        }

        // 防止请求重放(即防止别人拿到接口数据再次执行)，后端确保nonce只能用一次，放在redis中，有效期 20分钟
        String nonceKey = "SignatureVerificationFilter:nonce:" + nonce;
        if (Boolean.FALSE.equals(this.redisTemplate.opsForValue().setIfAbsent(nonceKey, "1", 20, TimeUnit.MINUTES))) {
            this.write(response, "nonce无效");
            logger.error("nonce无效");
            return false;
        }
        // 通过X-Nonce和X-Timestamp的搭配使用，防止请求重放
        // 假如10分钟内请求被重放，由于nonc在redis中的有效时间为20分钟，在后端被拦截，发现已经被使用，则第二次请求会失败
        // 假如20分钟后请求被重放，由于timeStamp时间戳有效时间为10分钟，在后端发现时间戳已经过期，则则第二次请求会失败

        // 请求体
        String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
        // 需要签名的数据：secretKey+nonce+timestampStr+body
        // 校验签名
        String data = String.format("%s%s%s%s", this.secretKey, nonce, timestampStr, body);
        if (!DigestUtil.md5Hex(data).equals(sign)) {
            write(response, "签名有误");
            logger.error("签名有误");
            return false;
        }
        return true;
    }

    /**
     * 向客户端写入响应信息
     *
     * @param response HTTP响应
     * @param msg 响应信息
     * @throws IOException 如果写入失败，抛出异常
     */
    private void write(HttpServletResponse response, String msg) throws IOException {
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.getWriter().write(JSONUtil.toJsonStr(msg));
    }
}
